#!/usr/bin/python

#########################
# Author: Andrew Bort
# Date: 2/27/11
#
#########################

from math import pi, sin, cos

from direct.showbase.ShowBase import ShowBase
from direct.task import Task
from direct.actor.Actor import Actor
from direct.interval.IntervalGlobal import *
from panda3d.core import Point3
from direct.showbase import DirectObject
from panda3d.core import Vec3,Vec4,BitMask32
from pandac.PandaModules import WindowProperties
from panda3d.core import CollisionTraverser,CollisionNode
from panda3d.core import CollisionHandlerQueue,CollisionRay
from pandac.PandaModules import ClockObject
import random, sys, os, math

class MyApp(ShowBase, DirectObject.DirectObject):
    def __init__(self):
        ShowBase.__init__(self)

        #Set fullscreen
        
        #wp = WindowProperties() 
        #wp.setFullscreen(True) 
        #base.win.requestProperties(wp)

        #Variable definitions
        self.center = self.findCenter()

        self.FPS = 30

        self.INITCAMX = 0.0
        self.INITCAMY = 0.0
        self.INITCAMZ = 3.0
        
        self.CAMX = self.INITCAMX
        self.CAMY = self.INITCAMY
        self.CAMZ = self.INITCAMZ

        self.CAMYAW = 0.0
        self.CAMPITCH = 0.0
        self.CAMROLL = 0.0
        self.YROTCONST = 30.0

        # how high the camera floats above the ground
        self.CAMERA_Z_OFFSET = self.CAMZ
        self.ZBOUND = -15

        self.ENVX = 0
        self.ENVY = 0
        self.ENVZ = -1

        self.CPXOFFSET = 0
        self.CPYOFFSET = 4
        self.CPZOFFSET = -6
        
        self.CPX = self.CAMX + self.CPXOFFSET
        self.CPY = self.CAMY + self.CPYOFFSET
        self.CPZ = self.CAMZ + self.CPZOFFSET

        self.CPSX = 1.5
        self.CPSY = 5
        self.CPSZ = 3
        
        self.MOVEINT = 40

        self.GRAVITY = 5
        self.JUMPVECTOR = 22
        self.UPSPEED = 0
        self.DOWNFORCE = 0
                
        #enemy
        self.enemies = {} # a record is: {int: ( eggmodel, direction )}, where direction = -1 for "toward start"
        self.enemyX = [self.CAMX - .5]
        self.enemyY = [self.CAMY + 20]
        self.enemyZ = [self.CAMZ - 2]
        
        self.goombaSX = .25
        self.goombaSY = .25
        self.goombaSZ = .25
        
        #self.enemyX[0] = self.CAMX
        #self.enemyY[0] = self.CAMY + 6
        #self.enemyZ[0] = self.CAMZ + self.CPZOFFSET

        #May not need all these variables
        self.STARTZ = self.CAMZ
        self.ZMIN = self.CAMZ
        self.ZMAX = 0.0

        self.startTime = 0
        self.lastTime = 0
        self.dTime = 0

        #Map for the player state
        
        #NOTE: Included forwarddir in state map, although it functions differently from
        #forward backward and jumping. Could possibly be replaced with global variable
        
        self.playerStateMap = {"forward":0, "backward":0, "forwarddir":1, "jumping": 0}
        base.win.setClearColor(Vec4(0,0,0,1))

        base.disableMouse()

        #Load the environment
        
        self.environ = loader.loadModel("models/ground/level_1-1_full")
        self.environ.reparentTo(render)
        self.environ.setPos(self.ENVX, self.ENVY, self.ENVZ)
        #self.environ.setHpr(90, 0, 0)

        #Load the clipping plane
        self.clippingPlane = loader.loadModel("models/clipping-plane/clipping_plane")
        self.clippingPlane.reparentTo(render)
        self.clippingPlane.setPos(self.CPX, self.CPY, self.CPZ)
        self.clippingPlane.setScale(self.CPSX, self.CPSY, self.CPSZ)
                
                #Load the Goombae
        self.enemies[0] = (loader.loadModel("models/goomba/goomba"), -1)
        enemy = self.enemies[0][0]
        enemy.reparentTo(render)
        enemy.setPos(self.enemyX[0], self.enemyY[0], self.enemyZ[0])
        enemy.setScale(self.goombaSX, self.goombaSY, self.goombaSZ)
        

        #Set the default camera location and direction
        camera.setPos(self.CAMX, self.CAMY, self.CAMZ)
        camera.setHpr(self.CAMYAW, self.CAMPITCH, self.CAMROLL)

        # We will detect the height of the terrain by creating a collision
        # ray and casting it downward toward the terrain. The ray will
        # start above the camera.
        # A ray may hit the terrain, or it may hit a rock or a tree.  If it
        # hits the terrain, we can detect the height.  If it hits anything
        # else, we rule that the move is illegal.
        # NOTE: This code taken from Roaming Ralph sample
        self.cTrav = CollisionTraverser()

        self.cameraGroundRay = CollisionRay()
        self.cameraGroundRay.setOrigin(0, 0, self.CAMZ)
        self.cameraGroundRay.setDirection(0,0,-1)
        self.cameraGroundCol = CollisionNode('cameraRay')
        self.cameraGroundCol.addSolid(self.cameraGroundRay)
        self.cameraGroundCol.setFromCollideMask(BitMask32.bit(0))
        self.cameraGroundCol.setIntoCollideMask(BitMask32.allOff())
        self.cameraGroundColNp = camera.attachNewNode(self.cameraGroundCol)
        self.cameraGroundHandler = CollisionHandlerQueue()
        self.cTrav.addCollider(self.cameraGroundColNp, self.cameraGroundHandler)

        # Uncomment this line to see the collision rays
        self.cameraGroundColNp.show()
       
        # Uncomment this line to show a visual representation of the 
        # collisions occuring
        self.cTrav.showCollisions(render)

        #Allow keystrokes to be accepted
        self.accept("escape", sys.exit)
        self.accept("w", self.setPlayerState, ["forward",1])
        self.accept("s", self.setPlayerState, ["backward",1])
        self.accept("w-up", self.setPlayerState, ["forward",0])
        self.accept("s-up", self.setPlayerState, ["backward",0])
        self.accept("space", self.setPlayerState, ["jumping", 1])

        self.accept("mouse1", self.reverseView)
        
        taskMgr.add(self.move,"moveTask")
        taskMgr.add(self.mouseTask,"mouseTask")
        taskMgr.add(self.jumpTask,"jumpTask")
    
        for key, enemyRecord in self.enemies.items() :
            #define first interval
            Sequence(LerpPosInterval(enemyRecord[0], 1, self.goombaTarget(enemyRecord, enemyRecord[0].getPos())),
                     Func(self.nextGoombaMove, enemyRecord, self.goombaTarget(enemyRecord, enemyRecord[0].getPos()))).start()
            
            
        # set framerate
        globalClock = ClockObject.getGlobalClock()
        globalClock.setMode(ClockObject.MLimited)
        globalClock.setFrameRate(self.FPS)

    #Change the  state of moving or clicking in a map
        
    def setPlayerState(self, state, value):
        #If the user is trying to jump, check if mario is in midair,
        #and if not, set his up vector and record the time of the jump

        #Otherwise, change the state as normal
        #if(state in 'jumping'):
           #print value
        if((state in 'jumping') & (self.playerStateMap[state] != 1)):
            # player is pushing jump key, so jump just started
            self.UPSPEED = self.JUMPVECTOR
            self.startTime = globalClock.getFrameTime()
            self.playerStateMap[state] = value          
        else:
            self.playerStateMap[state] = value
            
    #Task for moving camera (mario)
    def move(self, task):

        # save camera's initial position so that we can restore it,
        # in case player falls off the map or runs into something.
        startpos = camera.getPos()

        if(self.playerStateMap["forward"]!=0):
            self.CAMY = base.camera.getY() + self.MOVEINT * self.playerStateMap["forwarddir"] * globalClock.getDt()
            base.camera.setY(self.CAMY)
            self.clippingPlane.setY(self.CAMY + self.CPYOFFSET)
        if(self.playerStateMap["backward"]!=0):
            self.CAMY = base.camera.getY() - self.MOVEINT * self.playerStateMap["forwarddir"] * globalClock.getDt()
            base.camera.setY(self.CAMY)
            self.clippingPlane.setY(self.CAMY + self.CPYOFFSET)

        # Now check for collisions.
        # only if we're not jumping
        if(self.playerStateMap["jumping"]==0):
           self.cTrav.traverse(render)

           # Adjust camera's Z coordinate.  If camera's ray hit terrain,
           # update his Z. If it hit anything else, or didn't hit anything, put
           # him back where he was last frame.
           entries = []
           for i in range(self.cameraGroundHandler.getNumEntries()):
               entry = self.cameraGroundHandler.getEntry(i)
               entries.append(entry)
           entries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(),
                                        x.getSurfacePoint(render).getZ()))
           if (len(entries)>0) and (entries[0].getIntoNode().getName().startswith("Cube")):
                #print entries[0].getSurfacePoint(render).getZ()
                if(self.CAMZ - self.CAMERA_Z_OFFSET - entries[0].getSurfacePoint(render).getZ() <= 0):
                   camera.setZ(entries[0].getSurfacePoint(render).getZ() + self.CAMERA_Z_OFFSET)
                else:
                    self.UPSPEED = 0
                    self.startTime = globalClock.getFrameTime()
                    self.playerStateMap["jumping"] = 1 
                    #camera.setZ(entries[0].getSurfacePoint(render).getZ() + self.CAMERA_Z_OFFSET)
           else:
               camera.setPos(startpos)

        return task.cont


    #Method called when player clicks mouse. Reverses the camera yaw, and sets the state variable to
    #indicate whether it is going forward (1) or backward (-1). This is significant for the calculation
    #performed when the player is moving, because switching orientation switches the change of Y for
    #the 'W' and 'S' buttons. Initial orientation is forward (1)
    
    def reverseView(self):
        self.CAMYAW = (180 + self.CAMYAW) % 360
        base.camera.setHpr(self.CAMYAW, self.CAMPITCH, self.CAMROLL)
        self.playerStateMap["forwarddir"] *= -1
        
    #Task to control looking up and down with the mouse
            
    def mouseTask(self,task):  
       try: 
          # Get mouse coordinates from mouse watcher 
          if ( base.mouseWatcherNode.hasMouse() ):
             initCamY = self.CAMY
             initCamZ = self.CAMZ
             yPos=base.mouseWatcherNode.getMouseY()
             self.CAMPITCH = yPos * self.YROTCONST
             base.camera.setHpr(self.CAMYAW, self.CAMPITCH, self.CAMROLL)
             base.camera.setPos(self.CAMX, initCamY, initCamZ)
             base.win.movePointer(0, center[0], center[1]) 
           
       except: 
          # If mouse goes outside window, catch the 
          #    exception here and pass gracefully. 
          pass 
           
       return Task.cont

    def findCenter(self): 
       # Query the screen size 
       #   and calculate the center 
       props = base.win.getProperties() 
       winX = props.getXSize() 
       winY = props.getYSize() 
       return [winX / 2, winY / 2]

    #Task for making mario jump
    
    def jumpTask(self,task):
        # save camera's initial position so that we can restore it,
        # in case player falls off the map or runs into something.
        startpos = camera.getPos()

        curTime = globalClock.getFrameTime()
        self.dTime = curTime - self.lastTime
        if(self.playerStateMap["jumping"]!=0):
            # player is in the middle of a jumping state
            # calculate new pos, and speed/accel for next frame
            self.CAMZ = self.CAMZ + (self.UPSPEED * self.dTime)
            self.DOWNFORCE = self.DOWNFORCE + (self.GRAVITY * self.dTime)
            self.UPSPEED = self.UPSPEED - self.DOWNFORCE

            # Now check for collisions.
            self.cTrav.traverse(render)

            # Adjust camera's Z coordinate.  If camera's ray hit terrain,
            # update his Z. If it hit anything else, or didn't hit anything, put
            # him back where he was last frame.
            entries = []
            for i in range(self.cameraGroundHandler.getNumEntries()):
                entry = self.cameraGroundHandler.getEntry(i)
                entries.append(entry)
            entries.sort(lambda x,y: cmp(y.getSurfacePoint(render).getZ(),
                                         x.getSurfacePoint(render).getZ()))
            if (len(entries)>0) and (entries[0].getIntoNode().getName().startswith("Cube")):
                # get position camera would be at, on the surface
                surfacePos = entries[0].getSurfacePoint(render).getZ() + self.CAMERA_Z_OFFSET
    
                # check if camera is hitting terrain
                if ((self.UPSPEED < 0) and (self.CAMZ <= surfacePos)):
                    self.CAMZ = surfacePos
                    self.setPlayerState("jumping", 0)
                    self.UPSPEED = 0
                    self.DOWNFORCE = 0

            # reposition camera
            base.camera.setPos(self.CAMX, self.CAMY, self.CAMZ)
        self.lastTime = curTime
        return Task.cont
    
    def goombaTarget(self, enemyRecord, point3):
        direction = enemyRecord[1]
        return ( point3[0], point3[1] + 2*direction, point3[2] )
        
    def nextGoombaMove(self, goombaRecord, moveTarget) :
        newTarget = self.goombaTarget(goombaRecord, moveTarget)
        Sequence(LerpPosInterval(goombaRecord[0], 1, newTarget),
                 Func(self.nextGoombaMove, goombaRecord, newTarget)).start()
        #sequence.append(LerpPosInterval(goombaRecord[0], 3, self.goombaTarget(goombaRecord)))
    
app = MyApp()
app.run()
